package src.cl.movix.repositories.dao;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;

import org.apache.log4j.Logger;

import cl.movix.dao.StrategyRepository;
import cl.movix.model.customer.ICustomer;
import cl.movix.model.properties.IProperty;
import cl.movix.model.properties.PropertyMetaInfo;

public class PropertyDAO extends EntityDAO {

	private static Logger logger = Logger.getLogger(PropertyDAO.class);

	/** The SQL Property table's name */
	private static final String PROPERTY_TABLE = "properties";

	/** The SQL Property Meta-Info table's name */
	private static final String PROPERTY_META_INFO_TABLE = "property_types";

	private static final String COLUMN_VALUE_STRING = "value_str";
	private static final String COLUMN_VALUE_NUMBER = "value_num";
	private static final String COLUMN_VALUE_TIMEZONE = "value_tmz";
	private static final String COLUMN_VALUE_BOOLEAN = "value_bol";

	private static final List<Class<?>> supportedTypes = Arrays
			.asList(new Class<?>[] { String.class, Double.class, Long.class,
					Float.class, Date.class, Timestamp.class, Integer.class,
					Boolean.class });


	/**
	 * This method iterates a resultset and retrieves the Meta-info instances
	 * from the resultset.
	 * 
	 * @param resultSet
	 *            The result-set from which the meta-info properties are
	 *            retrieved.
	 * @return A list with all the properties retrieved from the resultset.
	 * @throws SQLException
	 *             Thrown if anything goes wrong.
	 */
	private static List<PropertyMetaInfo> getPropertiesMetaInfoFromResultSet(
			ResultSet resultSet) throws SQLException {

		List<PropertyMetaInfo> properties = new ArrayList<PropertyMetaInfo>();

		/* The result is processed in order to get the meta-info */
		while (resultSet.next()) {
			PropertyMetaInfo propertyMetaInfo;
			try {
				propertyMetaInfo = PropertyMetaInfo.getInstance(
						resultSet.getString("name"),
						resultSet.getString("module"), String.class);

				/*
				 * If the property meta-info instance is not persisted the data
				 * from the result set is set into it
				 */
				if (!PropertyDAO.isPersisted(propertyMetaInfo))
					PropertyDAO.setPropertyFromResultset(propertyMetaInfo,
							resultSet);

			} catch (ClassNotFoundException e) {
				logger.error(e);
				continue;
			}

			/* The property is add to the list */
			properties.add(propertyMetaInfo);
		}

		return properties;
	}

	/**
	 * This method is responsible of obtaining data from a resultset and setting
	 * it into the meta-info object.
	 * 
	 * @param propertyMetaInfo
	 *            The object into which the values of the resultset are to be
	 *            set.
	 * @param resultset
	 *            The resultset containing the information
	 * @throws SQLException
	 *             Thrown if anything goes wrong in here.
	 * @throws ClassNotFoundException
	 */
	private static void setPropertyFromResultset(
			PropertyMetaInfo propertyMetaInfo, ResultSet resultset)
			throws SQLException, ClassNotFoundException {

		propertyMetaInfo.setId(resultset.getLong("id"));
		propertyMetaInfo.setType(Class.forName(resultset.getString("type")));
		propertyMetaInfo.setInsertStrategy(StrategyRepository.valueOf(resultset
				.getString("strategy_insert")));
		propertyMetaInfo.setUpdateStrategy(StrategyRepository.valueOf(resultset
				.getString("strategy_update")));
		propertyMetaInfo.setDeleteStrategy(StrategyRepository.valueOf(resultset
				.getString("strategy_delete")));
		propertyMetaInfo.setSelectStrategy(StrategyRepository.valueOf(resultset
				.getString("strategy_select")));
		propertyMetaInfo.setHistoryStrategy(StrategyRepository
				.valueOf(resultset.getString("strategy_history")));
	}

	/**
	 * This method is responsible of determining whether a given Meta-Info
	 * Property is persisted or is just a POJO. The criteria used to determine
	 * this status is by analyzing the ID value of the meta-info property. If
	 * this value is a positive long, it's assumed that the property is
	 * persisted.
	 * 
	 * @param propertyMetaInfo
	 *            The meta-info property to be examined for persistence status.
	 * @return <code>true</code> if the property is persisted already and
	 *         <code>false</code> otherwise.
	 */
	private static boolean isPersisted(PropertyMetaInfo propertyMetaInfo) {

		/* By now, the main criteria used, is to have a positive ID */
		return propertyMetaInfo.getId() > 0;

	}

	/**
	 * This method is responsible to return the name of the column associated to
	 * a given type of property.
	 * 
	 * @param theMetaInfo
	 *            The meta-info property.
	 * @return A String representing the exact column name where the value is to
	 *         be inserted.
	 */
	private static String getValueColumn(PropertyMetaInfo theMetaInfo) {

		String type = theMetaInfo.getType().getCanonicalName();

		if (type.equalsIgnoreCase("java.lang.Integer")
				|| type.equalsIgnoreCase("java.lang.Float")
				|| type.equalsIgnoreCase("java.lang.Double"))
			return COLUMN_VALUE_NUMBER;

		if (type.equalsIgnoreCase("java.lang.String"))
			return COLUMN_VALUE_STRING;

		if (type.equalsIgnoreCase("java.util.Date")
				|| type.equalsIgnoreCase("java.sql.Timestamp"))
			return COLUMN_VALUE_TIMEZONE;

		if (type.equalsIgnoreCase("java.lang.Boolean"))
			return COLUMN_VALUE_BOOLEAN;
		
		throw new IllegalArgumentException(
				"The Meta-info Property type is not supported: "
						+ theMetaInfo.getType());
	}

	/**
	 * The super-class constructor.
	 * 
	 * @param connection
	 *            The connection for the statements.
	 */
	public PropertyDAO(Connection connection) {
		super(connection);
	}

	/**
	 * This method is responsible of persisting a property, taking care whether
	 * it is also necessary to persist the MetaInfo property.
	 * 
	 * @param property
	 *            The property to be persisted.
	 * @param customer
	 * @throws SQLException
	 *             Thrown if anything happens while inserting either the
	 *             property or the meta-inf.
	 * @throws ClassNotFoundException
	 */
	public void persist(IProperty property, ICustomer customer)
			throws SQLException, ClassNotFoundException {
	
		/* TODO: A robust meta-info for the property is obtained */
		PropertyMetaInfo theMetaInfo = this.getMetaInfo(property);
		String valueColumn = getValueColumn(theMetaInfo);
	
		/* The SQL Prepared Statement to be executed */
		String INSERT_STMT = "INSERT INTO " + PROPERTY_TABLE
				+ " (customer_id, property_type_id, module_id, " + valueColumn
				+ ") VALUES (?, ?, ?, ?)";
	
		/* TODO The SQL prepared statement is set */
		PreparedStatement insertStmt;
		insertStmt = connection.prepareStatement(INSERT_STMT);
		insertStmt.setLong(1, customer.getId());
		insertStmt.setLong(2, property.getMetaInfo().getId());
		insertStmt.setLong(3, property.getModule().getModuleInstance());
	
		/* The object is set with the dynamic type */
		insertStmt.setObject(4, property.getValue());
	
		// TODO: Agregar soporte para TimeStamp despues de cmabiar el value a
		// objet.
		// case "java.util.Date":
		// insertStmt.setTimestamp(4,
		// Double.parseDouble(property.getValue().toString()));
		// break
		// case "java.sql.Timestamp":
		// return COLUMN_VALUE_TIMEZONE;
	
		insertStmt.execute();
	}

	/**
	 * This method is reponsible of upda
	 * @param customer
	 * @param property
	 */
	public void update(ICustomer customer, IProperty property) {
	
	}

	/**
	 * This method is responsible of retrieving all the properties from the
	 * DataBase and adding them to the PropertyMetaInfo set.
	 * 
	 * @return The list of property meta-inf retrieved.
	 * @throws SQLException
	 *             Thrown if anything happens during the select.
	 */
	public List<PropertyMetaInfo> getAllPropertiesMetaInfo()
			throws SQLException {

		/* The SQL statement is executed */
		String select_sql = "SELECT * FROM " + PROPERTY_TABLE;
		Statement stmt = this.connection.createStatement();
		ResultSet resultSet = stmt.executeQuery(select_sql);

		return PropertyDAO.getPropertiesMetaInfoFromResultSet(resultSet);

	}

	/**
	 * This method is responsible of retrieving all the properties related with
	 * the users.
	 * 
	 * @return A PropertyMetaInfo <code>List</code> of customer properties.
	 * @throws SQLExceptionv
	 *             Thrown if anything happens during the select.
	 */
	public List<PropertyMetaInfo> getAllCustomerPropertiesMetaInfo()
			throws SQLException {

		/* The SQL statement is executed */
		String select_sql = "SELECT * FROM properties INNER JOIN customer_properties ON customer_properties.id_property = properties.id";
		Statement stmt = this.connection.createStatement();
		ResultSet resultSet = stmt.executeQuery(select_sql);

		return PropertyDAO.getPropertiesMetaInfoFromResultSet(resultSet);
	}

	/**
	 * This method is responsible of loading all the user properties. To do
	 * this, the method <code>getAllCustomerPropertiesMetaInfo</code> is called.
	 * When called, it loads the properties.
	 * 
	 * @throws SQLException
	 *             Thrown if something happens.
	 */
	public void loadCustomerProperties() throws SQLException {

		/* The customer's property meta-info are retrieved */
		this.getAllCustomerPropertiesMetaInfo();

	}

	/**
	 * TODO Doc.
	 * 
	 * @param property
	 * @return
	 * @throws SQLException
	 * @throws ClassNotFoundException
	 */
	private PropertyMetaInfo getMetaInfo(IProperty property)
			throws SQLException, ClassNotFoundException {

		/*
		 * If the property meta-info has a valid positive integer as ID, the
		 * it's a valid meta-info property
		 */
		if (property.getMetaInfo().getId() > 0)
			return property.getMetaInfo();

		/* Otherwise, we obtain a valid one */
		return this.getMetaInfoProperty(property.getName(), property
				.getMetaInfo().getModule(), property.getMetaInfo().getType());
	}

	/**
	 * This method is responsible of returning the property whether it exist or
	 * not directly from the database. More over, it's updates the business
	 * object with a fresh information.
	 * 
	 * @param name
	 *            The name of the property.
	 * @param moduleName
	 *            The module's name to which the property belongs to.
	 * @param type
	 *            The type of the module, in case it does not exists.
	 * @return A PropertyMetaInfo object directly obtained from the Database.
	 * @throws SQLException
	 *             Thrown whenever something goes wrong in the database.
	 * @throws ClassNotFoundException
	 */
	private PropertyMetaInfo getMetaInfoProperty(String name,
			String moduleName, Class<?> type) throws SQLException,
			ClassNotFoundException {

		try {
			return this.findMetaInfoProperty(name, moduleName);
		} catch (PropertyNotFoundException e) {
			/* Create the <code>PropertyMetaInfo</code> object */
			PropertyMetaInfo pmi = PropertyMetaInfo.getInstance(name,
					moduleName, type);
			this.persist(pmi);
			return pmi;
		}
	}

	/**
	 * This method finds a <code>PropertyMetaInfo</code> object in the
	 * repository. It is supposed to find it and return the object, and an
	 * exception is thrown if the property is not found.
	 * 
	 * If the object is found, the business object of the Multiton is refreshed
	 * with this new information.
	 * 
	 * @param name
	 *            The property's name.
	 * @param module
	 *            The property's module.
	 * @return The <code>PropertyMetaInfo</code> object.
	 * @throws SQLException
	 *             Thrown if anything goes wrong with the repository.
	 * @throws PropertyNotFoundException
	 *             If there is no property with that name.
	 */
	private PropertyMetaInfo findMetaInfoProperty(String name, String module)
			throws SQLException, PropertyNotFoundException {

		/* The query for looking up the customer's property */
		String selectSQL = "SELECT * FROM " + PROPERTY_META_INFO_TABLE
				+ " where module = ? AND name = ?";

		PreparedStatement selectStmt = this.connection
				.prepareStatement(selectSQL);
		selectStmt.setString(1, module);
		selectStmt.setString(2, name);

		/* The property's name is searched in the repository */
		ResultSet result = selectStmt.executeQuery();

		/* If there is no result, the property does not exist! */
		if (!result.next())
			throw new PropertyNotFoundException(name);

		/* Otherwise, the property Meta Info is returned with refreshed data */
		PropertyMetaInfo pmi;
		try {
			pmi = PropertyMetaInfo.getInstance(name, module,
					Class.forName(result.getString("type")));
		} catch (ClassNotFoundException e) {
			logger.fatal(
					"There is an inconsistence type stored in the data base for the type of name '"
							+ name + "' and module '" + module + ".", e);
			throw new PropertyNotFoundException(name + " / " + module);
		}
		pmi.setId(result.getLong("id"));
		pmi.setInsertStrategy(StrategyRepository.valueOf(result
				.getString("strategy_insert")));
		pmi.setUpdateStrategy(StrategyRepository.valueOf(result
				.getString("strategy_update")));
		pmi.setSelectStrategy(StrategyRepository.valueOf(result
				.getString("strategy_select")));
		pmi.setHistoryStrategy(StrategyRepository.valueOf(result
				.getString("strategy_history")));

		/* The refreshed instance is returned */
		return pmi;
	}

	/**
	 * This method is responsible of persisting the Meta-Info Property Object
	 * given as argument.
	 * 
	 * @param pmi
	 *            The Meta-Info Property to be persisted.
	 * @throws IllegalArgumentException
	 *             Thrown if the PropertyMetaInfo object is not valid to be
	 *             inserted.
	 * @throws SQLException
	 *             If something happens while inserting.
	 */
	private void persist(PropertyMetaInfo pmi) throws IllegalArgumentException,
			SQLException {

		/* The Meta-Info Property is validated to be sufficient */
		validArgymentForInsert(pmi);

		/* The SQL Prepared Statement to be executed */
		String INSERT_STMT = "INSERT INTO " + PROPERTY_META_INFO_TABLE
				+ " (name, type, module, strategy_insert, "
				+ "strategy_update, strategy_select, "
				+ "strategy_delete, strategy_history) "
				+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING id";

		/* TODO The SQL prepared statement is set */
		PreparedStatement insertStmt;

		insertStmt = connection.prepareStatement(INSERT_STMT);
		insertStmt.setString(1, pmi.getName());
		insertStmt.setString(2, pmi.getType().getCanonicalName());
		insertStmt.setString(3, pmi.getModule());
		insertStmt.setString(4, pmi.getInsertStrategy().toString());
		insertStmt.setString(5, pmi.getUpdateStrategy().toString());
		insertStmt.setString(6, pmi.getSelectStrategy().toString());
		insertStmt.setString(7, pmi.getDeleteStrategy().toString());
		insertStmt.setString(8, pmi.getHistoryStrategy().toString());

		insertStmt.execute();
		insertStmt.getResultSet().next();

		/* The ID created is set to the object */
		pmi.setId(insertStmt.getResultSet().getLong("id"));
	}

	/**
	 * THis method is responsible to validate the metainfo property object
	 * subject to be inserted whether this object has the minimum necessary
	 * fields non-null so it can be inserted.
	 * 
	 * @param pmi
	 *            The metaInfo Property.
	 * @throws IllegalArgumentException
	 *             Throws if ever this pmi does not satisfies the conditions.
	 */
	private static void validArgymentForInsert(PropertyMetaInfo pmi)
			throws IllegalArgumentException {

		/* The name can not be null or empty */
		if (pmi.getName() == null || pmi.getName().equals(""))
			throw new IllegalArgumentException(
					"The meta-info property's name can not be null or empty");

		/* The property's module can not be null or empty */
		if (pmi.getModule() == null || pmi.getModule().equals(""))
			throw new IllegalArgumentException(
					"The meta-info property's module can not be null or empty");

		/* The type can not be null or empty */
		if (pmi.getType() == null || pmi.getType().equals(""))
			throw new IllegalArgumentException(
					"The meta-info property's type can not be null or empty");

		if (!supportedTypes.contains(pmi.getType()))
			throw new IllegalArgumentException(
					"The meta-info property's type (" + pmi.getType()
							+ ") is not supported.");

	}
}
